package com.datalayer.orm;


import com.container.app.annotations.Component;
import com.datalayer.annotations.*;
import com.datalayer.utils.Column;
import com.datalayer.utils.OrmProcessing;
import com.datalayer.utils.Pair;
import com.datalayer.utils.SQLCommands;

import java.lang.annotation.AnnotationTypeMismatchException;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

@Component
public class MyORM implements ORM {

    private ThreadLocal<Connection> perThreadConnection = new ThreadLocal<Connection>();
    private SQLCommands sqlCommands;


    public Connection initConnection(String url, String userName, String pass) {

        Connection myConnection = perThreadConnection.get();

        if (myConnection == null) {
            try {
                myConnection = DriverManager.getConnection(url, userName, pass);
                perThreadConnection.set(myConnection);
            } catch (SQLException e) {
                throw new IllegalArgumentException("Can't init connection with parameters: " + url + "; " + userName + "; " + pass, e);
            }
        }

        sqlCommands = new SQLCommands(myConnection);
        return myConnection;

    }

    public void closeConnection() throws SQLException {

        Connection myConnection = perThreadConnection.get();
        perThreadConnection.remove();
        if (myConnection != null)
            myConnection.close();


    }

    public Statement getStatement() throws IllegalStateException, UnsupportedOperationException {

        Connection connection = perThreadConnection.get();
        if (connection == null) {

            throw new IllegalStateException("The connection is not initialised. ");

        }
        try {

            Statement st = perThreadConnection.get().createStatement();
            if (st == null)
                throw new UnsupportedOperationException("Can't get statement from current connection.");
            return st;

        } catch (SQLException e) {
            throw new UnsupportedOperationException("Can't get statement from current connection.", e);
        }
    }


    /**
     * Removes an object from DB (removes it's corresponding row) recursively.
     * Before removing the corresponding column, it removes any references to it through foreign keys.
     *
     * @param object The instance of the class corresponding to a row in the class table.
     * @throws SQLException if can't remove object from database
     */
    public <T> void removeObjectFromDB(T object) throws SQLException, AnnotationTypeMismatchException, IncompleteAnnotationException, UnsupportedOperationException {

        removeObjectFromDBwithReference(object, null);


    }

    private void removeObjectFromDBwithReference(Object object, Object source) throws SQLException, UnsupportedOperationException, AnnotationTypeMismatchException, IncompleteAnnotationException {

        List ro = OrmProcessing.getReferencedObjects(object, source);

        if (ro != null) {
            for (Object o : ro) {
                removeObjectFromDBwithReference(o, object);
            }
        }

        deleteSingleObject(object);
    }

    /**
     * Removes an entity from DB (removes it's corresponding table) recursively.
     * Before removing the corresponding table, it removes any references to it through foreign keys.
     *
     * @param entityClass The class whose table is to be droped.
     * @throws SQLException
     */
    public void removeEntityFromDB(Class entityClass) throws SQLException , UnsupportedOperationException, AnnotationTypeMismatchException, IncompleteAnnotationException{

        removeEntityFromDBwithReference(entityClass, null);
    }

    private void removeEntityFromDBwithReference(Class entityClass, Class source) throws SQLException , UnsupportedOperationException, AnnotationTypeMismatchException, IncompleteAnnotationException {

        try {

            List<Class> rc = OrmProcessing.getReferencedObjects(entityClass, source);


            if (rc != null) {
                for (Class c : rc) {
                    removeEntityFromDBwithReference(c, entityClass);
                }
            }

            String tableName = (String) OrmProcessing.getAnnotationValue(1, entityClass, Entity.class, "tableName");

            sqlCommands.deleteTable(tableName);

        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(Entity.class, "tablename");
        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(Entity.class, "tablename");
        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(Entity.class, "tablename");

        }

    }

    /**
     * Maps an entity class to a table in the DB, creating it afterwards.
     * <p/>
     * It keeps into account primary and foreign key constraints by
     * creating the tables that it references beforehand.
     *
     * @param entityClass The class to be mapped to a sql table.
     * @throws SQLException can't create table inside database
     */
    public void createTableFromEntity(Class entityClass) throws SQLException, AnnotationTypeMismatchException, IncompleteAnnotationException {

        try {

            String tableName = (String) OrmProcessing.getAnnotationValue(1, entityClass, Entity.class, "tableName"), constraints;
            List<Column> attributes;
            Method methods[] = entityClass.getDeclaredMethods();


            Pair<String, List<Column>> atr_constr = getAttributesConstraints(methods);


            constraints = atr_constr.getFirst();
            attributes = atr_constr.getSecond();


            if (!constraints.equals("")) {
                constraints = constraints.substring(0, constraints.length() - 2);
            }

            sqlCommands.createTable(tableName, attributes, constraints);


        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        }
    }

    /**
     * Inserts an object instance into the DB.
     * <p/>
     * It keeps into account primary and foreign key constraints by
     * inserting the values that it references beforehand.
     * <p/>
     * It may create tables if necessary.
     *
     * @param object The object to be inserted into the database as a row in the class's corresponding table.
     * @param <T>    Object class type.
     * @throws SQLException
     */
    public <T> void insertObject2DB(T object) throws SQLException, AnnotationTypeMismatchException, UnsupportedOperationException, IncompleteAnnotationException {


        Class entityClass = object.getClass();
        String tableName;

        try {
            tableName = (String) OrmProcessing.getAnnotationValue(1, entityClass, Entity.class, "tableName");
        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        }

        Method methods[] = entityClass.getDeclaredMethods();

        createTableIfDoesNotExist(entityClass);

        HashMap<String, String> hm = getInsertValues(object, methods);

        try {

            sqlCommands.insertTable(tableName, hm);

        } catch (SQLException e) {

            if (e.getErrorCode() == 1062) {

                throw new SQLIntegrityConstraintViolationException("The inserted values are not unique.",e);
            }
        }


    }

    /**
     * Create a Query Object deriving from an input Sring.
     * <p/>
     * Used to transform MyORM queries into SQL queries and
     * get list of resulting objects.
     *
     * @param query Query String that respects MyQuery rules.
     * @return MyQuery object.
     */
    public MyQuery makeQuery(String query) {
        return new MyQuery(query, this);
    }


    /**
     * Creates a table if it does not already exist in the DB.
     *
     * @param entityClass the clas mapped to the table
     * @throws SQLException
     */
    public void createTableIfDoesNotExist(Class entityClass) throws SQLException, IncompleteAnnotationException {


        int nr = 0;
        String tableName;
        try {
            tableName = (String) OrmProcessing.getAnnotationValue(1, entityClass, Entity.class, "tableName");
        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        }
        String query = "show tables like '" + tableName + "'";
        Statement st = getStatement();

        ResultSet rs = st.executeQuery(query);

        while (rs.next()) {
            nr++;
        }

        if (nr == 0) {
            createTableFromEntity(entityClass);
        }


    }


    /* Auxiliary private methods used to implement main features*/


    /**
     * Gets the attribute name, type and constraint for a method.
     *
     * Used to create a table.
     *
     * @param m the Method for which we want to obtain information
     * @return a pair of a String representing  the primary key name or null and a Column object containting name, type and constraint
     *
     */
    private Pair<String, Column> getAttributes(Method m) throws AnnotationTypeMismatchException {

        Pair<String, Column> result;
        Column col = null;
        String constraint, type, name, pkey = null;

        try {

            if (m.isAnnotationPresent(Attribute.class)) {

                constraint = (String) OrmProcessing.getAnnotationValue(2, m, Constraint.class, "constraintName");
                name = (String) OrmProcessing.getAnnotationValue(2, m, Attribute.class, "attributeName");
                type = (String) OrmProcessing.getAnnotationValue(2, m, Attribute.class, "type");

                if (constraint == null)
                    constraint = "";
                if (constraint.contains("primary") && constraint.contains("key")) {
                    pkey = name;
                    constraint = "";
                }

                col = new Column(name, type, constraint);


            }

            result = new Pair<String, Column>(pkey, col);
            return result;

        } catch (NoSuchMethodException e) {
            throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
        } catch (IllegalAccessException e) {
            throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
        } catch (InvocationTargetException e) {
            throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
        }


    }

    /**
     * Gets the referenced  columns from a method.
     * <p/>
     * It resolves foreign reference constraints: it creates a referenced table if it doesn't already exist
     *
     * @param m the method
     * @return a Pair of the foreign key constraint String and a List of columns that represent foreign keys
     *
     * @throws SQLException
     */
    private Pair<String, List<Column>> getAssociations(Method m) throws SQLException, AnnotationTypeMismatchException, IncompleteAnnotationException {

        Pair<String, List<Column>> result;
        String foreignTable, constraints = "";
        List<Column> attributes = new LinkedList<Column>();
        Column col;

        try {

            if (m.isAnnotationPresent(Association.class)) {

                Class foreignCls = m.getReturnType(); // only one2* have foreign keys
                foreignTable = (String) OrmProcessing.getAnnotationValue(1, foreignCls, Entity.class, "tableName");
                HashMap<String, Column> references = OrmProcessing.joinReferences(foreignCls, m);

                constraints += OrmProcessing.getReferenceConstraint(foreignTable, references);


                for (Map.Entry<String, Column> entry : references.entrySet()) {
                    col = new Column(entry.getKey(), entry.getValue().getType(), "");
                    attributes.add(col);
                }

                if (references.size() > 0) {
                    createTableIfDoesNotExist(foreignCls);

                }

            }

            result = new Pair<String, List<Column>>(constraints, attributes);
            return result;

        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(Entity.class, "tableName");
        }

    }


    /**
     * Obtains the attributes and constraints of a class's methods used to create a  SQL table.
     *
     * @param methods vector of a class's methods
     * @return a Pair of a String representing the foreigh/primary key constraints and a list of columns representing the method's attributes
     *
     */
    private Pair<String, List<Column>> getAttributesConstraints(Method methods[]) throws SQLException, AnnotationTypeMismatchException, IncompleteAnnotationException {

        String constraints = "";
        Pair<String, List<Column>> result, associations;
        List<Column> attributes = new LinkedList<Column>();
        Pair<String, Column> attribute;
        List<String> pkeys = new LinkedList<String>();


        for (Method m : methods) {
            try {

                String direction = (String) OrmProcessing.getAnnotationValue(2, m, Direction.class, "value");
                if ("get".equals(direction)) {

                    attribute = getAttributes(m);

                    if (attribute.getSecond() != null) attributes.add(attribute.getSecond());
                    if (attribute.getFirst() != null) pkeys.add(attribute.getFirst());


                    associations = getAssociations(m);

                    constraints += associations.getFirst();
                    attributes.addAll(associations.getSecond());


                }
            } catch (NoSuchMethodException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (IllegalAccessException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (InvocationTargetException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            }

        }

        constraints += OrmProcessing.getPrimaryKeyConstraint(pkeys);
        result = new Pair<String, List<Column>>(constraints, attributes);
        return result;

    }


    /**
     * Obtains a pair of the column name and value mapped from the method
     * of an instance of the class mapped to the table.
     *
     * @param object instance of the classed mapped to a table
     * @param m      the class's method that maps a field to a column
     * @param <T>    type of object
     * @return a pair of the column name and value
     */
    private <T> Pair<String, String> getAttributeValues(T object, Method m) throws AnnotationTypeMismatchException {

        Pair<String, String> result = null;
        String attrName, attrValue;

        try {

            if (m.isAnnotationPresent(Attribute.class)) {

                m.setAccessible(true);
                attrName = (String) OrmProcessing.getAnnotationValue(2, m, Attribute.class, "attributeName");

                attrValue = m.invoke(object).toString();
                if (m.getReturnType().equals(String.class))
                    attrValue = "'" + attrValue + "'";
                if (attrName != null && attrValue != null) result = new Pair<String, String>(attrName, attrValue);

            }

            return result;

        } catch (NoSuchMethodException e) {
            throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
        } catch (IllegalAccessException e) {
            throw new AnnotationTypeMismatchException(m, "Method is either not annotated Correctly or can't be invoked.");
        } catch (InvocationTargetException e) {
            throw new AnnotationTypeMismatchException(m, "Method is either not annotated Correctly or can't be invoked.");
        }

    }


    /**
     * Gets the referenced  columns names and values
     * from a method corresponding to a certain object of a classed mapped to a table.
     * <p/>
     * It resolves foreign reference constraints: it inserts a referenced object into
     * the corresponding table if it doesn't already exist
     *
     * @param object the object of a class mapped to a table
     * @param m      the class's method that maps an association
     * @param <T>    type of object
     * @return a hashmap of attribute name and value
     */
    private <T> HashMap<String, String> getAssociationValues(T object, Method m) throws SQLException, AnnotationTypeMismatchException, UnsupportedOperationException {

        HashMap<String, String> hm = new HashMap<String, String>();
        String attrName, attrValue;

        try {

            if (m.isAnnotationPresent(Association.class)) {
                Class foreignCls = m.getReturnType();    // only one2* have foreign keys
                HashMap<String, Column> joinColums = OrmProcessing.joinReferences(foreignCls, m);
                m.setAccessible(true);
                Object foreignEntity = m.invoke(object);

                if (joinColums.size() > 0) {
                    try {

                        insertObject2DB(foreignEntity);

                    } catch (SQLIntegrityConstraintViolationException e) {
                        if (!e.getMessage().equals("The inserted values are not unique."))
                            throw new SQLException(e);
                    }
                }

                for (Map.Entry<String, Column> entry : joinColums.entrySet()) {
                    attrName = entry.getKey();
                    attrValue = OrmProcessing.getColumnValueByName(foreignEntity, entry.getValue().getName()).toString();
                    hm.put(attrName, attrValue);
                }


            }
            return hm;

        } catch (InvocationTargetException e) {
            throw new UnsupportedOperationException("Can't obtain foreign class",e);
        } catch (IllegalAccessException e) {
            throw new UnsupportedOperationException("Can't obtain foreign class",e);
        }


    }


    /**
     * Gets a mapping between column names and values  obtained from an instance of a class that is mapped to a table.
     *
     * @param object  instance of a class that is mapped to a table
     * @param methods vector  of class's methods
     * @param <T>     type of object
     * @return hashmap of column names and values corresponding to an instance of a class that is mapped to a table
     *
     */
    private <T> HashMap<String, String> getInsertValues(T object, Method methods[]) throws SQLException, AnnotationTypeMismatchException, UnsupportedOperationException {

        HashMap<String, String> hm = new HashMap<String, String>();
        Pair<String, String> attributes_values;

        for (Method m : methods) {
            try {
                String direction = (String) OrmProcessing.getAnnotationValue(2, m, Direction.class, "value");
                if ("get".equals(direction)) {

                    attributes_values = getAttributeValues(object, m);
                    if (attributes_values != null) hm.put(attributes_values.getFirst(), attributes_values.getSecond());
                    hm.putAll(getAssociationValues(object, m));

                }

            } catch (NoSuchMethodException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (IllegalAccessException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (InvocationTargetException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            }

        }
        return hm;

    }


    /**
     * Deletes the corresponding row of an object from the class's table.
     * <p/>
     * It uses the column names and values as the conditions to find the corresonding row.
     *
     * @param object object whose corresponding row will be deleted
     *
     * @throws SQLException
     */
    private void deleteSingleObject(Object object) throws AnnotationTypeMismatchException, IncompleteAnnotationException, SQLException {

        Method methods[] = object.getClass().getDeclaredMethods();

        HashMap<String, Object> conditions = new HashMap<String, Object>();


        for (Method m : methods) {
            try {

                String direction = (String) OrmProcessing.getAnnotationValue(2, m, Direction.class, "value");
                if ("get".equals(direction) && m.isAnnotationPresent(Attribute.class)) {

                    m.setAccessible(true);
                    String column_value = m.invoke(object).toString();
                    if (m.getReturnType().equals(String.class))
                        column_value = "'" + column_value + "'";
                    conditions.put((String) OrmProcessing.getAnnotationValue(2, m, Attribute.class, "attributeName"), column_value);
                }
            } catch (NoSuchMethodException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (IllegalAccessException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            } catch (InvocationTargetException e) {
                throw new AnnotationTypeMismatchException(m, "Method is not annotated Correctly.");
            }

        }

        try {
            deleteRow(conditions, (String) OrmProcessing.getAnnotationValue(1, object.getClass(), Entity.class, "tableName"));
        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(Entity.class, "tablename");
        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(Entity.class, "tablename");
        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(Entity.class, "tablename");
        }
    }


    /**
     * Deletes a row with a certain set of conditions from a certain table.
     *
     * @param eqRef     conditions
     * @param tableName table name
     * @throws SQLException
     */
    private void deleteRow(HashMap<String, Object> eqRef, String tableName) throws SQLException {

        List<String> conditions = OrmProcessing.addEqual(eqRef);

        if (eqRef.size() == 0)
            return;


        sqlCommands.deleteFromTable(tableName, conditions);


    }


}


